from django.db import transaction
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers

from sysreptor import signals as sysreptor_signals
from sysreptor.pentests.models import FindingTemplate, UploadedTemplateImage
from sysreptor.pentests.models.project import PentestProject
from sysreptor.pentests.models.template import FindingTemplateTranslation
from sysreptor.pentests.serializers.common import LockInfoSerializer
from sysreptor.utils.fielddefinition.serializers import serializer_from_definition
from sysreptor.utils.history import (
    bulk_create_with_history,
    bulk_delete_with_history,
    bulk_update_with_history,
    history_context,
)
from sysreptor.utils.utils import omit_keys


class FindingTemplateTranslationShortDataSerializer(serializers.Serializer):
    title = serializers.CharField()


class FindingTemplateTranslationShortSerializer(serializers.ModelSerializer):
    data = FindingTemplateTranslationShortDataSerializer(source='*')

    class Meta:
        model = FindingTemplateTranslation
        fields = [
            'id', 'created', 'updated',
            'language', 'status', 'is_main',
            'risk_score', 'risk_level', 'data',
        ]


class FindingTemplateTranslationSerializer(FindingTemplateTranslationShortSerializer):
    def get_fields(self):
        return super().get_fields() | {
            'data': serializer_from_definition(definition=FindingTemplate.field_definition),
        }

    def to_representation(self, instance):
        # Skip undefined data fields not present in translation.data, but defined by the serializer
        res = super().to_representation(instance)
        res['data'] = omit_keys(res['data'], set(res['data'].keys()) - set(instance.data.keys()))
        return res

    def create(self, validated_data):
        data = validated_data.pop('data')
        instance = FindingTemplateTranslation(template=self.context['template'], **validated_data)
        instance.update_data(data)
        instance.save()
        return instance

    def update(self, instance, validated_data):
        instance.update_data(validated_data.pop('data', {}))
        return super().update(instance, validated_data)


class FindingTemplateTranslationCreateMainSerializer(FindingTemplateTranslationSerializer):
    is_main = serializers.BooleanField(default=False)

    class Meta(FindingTemplateTranslationSerializer.Meta):
        extra_kwargs = {'id': {'read_only': False, 'required': False, 'allow_null': True}}


class FindingTemplateShortSerializer(serializers.ModelSerializer):
    translations = FindingTemplateTranslationShortSerializer(many=True, read_only=True)
    details = serializers.HyperlinkedIdentityField(view_name='findingtemplate-detail', read_only=True)
    images = serializers.HyperlinkedIdentityField(view_name='uploadedtemplateimage-list', lookup_url_kwarg='template_pk', read_only=True)

    class Meta:
        model = FindingTemplate
        fields = [
            'id', 'created', 'updated', 'details', 'images',
            'usage_count', 'source', 'tags', 'translations',
        ]
        read_only_fields = ['usage_count']
        extra_kwargs = {
            'tags': {'required': False, 'allow_empty': True},
        }


class FindingTemplateSerializer(FindingTemplateShortSerializer):
    lock_info = LockInfoSerializer()
    copy_of = serializers.PrimaryKeyRelatedField(read_only=True)
    translations = FindingTemplateTranslationCreateMainSerializer(many=True, allow_empty=False)

    class Meta(FindingTemplateShortSerializer.Meta):
        fields = FindingTemplateShortSerializer.Meta.fields + ['lock_info', 'copy_of']

    def validate_translations(self, value):
        if len(list(filter(lambda t: t.get('is_main'), value))) != 1:
            raise serializers.ValidationError('No main translation given')
        if len(set(map(lambda t: t.get('language'), value))) != len(value):
            raise serializers.ValidationError('Duplicate template language detected')
        return value

    @transaction.atomic
    @history_context()
    def create(self, validated_data):
        translations_data = validated_data.pop('translations')
        # Create template
        instance = FindingTemplate(**validated_data | {'skip_post_create_signal': True})
        instance.save_without_historical_record()
        # Create translations
        translations = []
        for trd in translations_data:
            trd.pop('id', None)
            is_main = trd.pop('is_main', False)
            data = trd.pop('data', {})
            tr = FindingTemplateTranslation(template=instance, **trd)
            tr.update_data(data)
            tr.update_risk_score()
            translations.append(tr)
            if is_main:
                instance.main_translation = tr
        bulk_create_with_history(FindingTemplateTranslation, translations)
        # Save template.main_translation
        instance._history_type = '+'
        instance.save()
        del instance._history_type
        if not validated_data.get('skip_post_create_signal'):
            sysreptor_signals.post_create.send(sender=instance.__class__, instance=instance)
        return instance

    @transaction.atomic()
    @history_context()
    def update(self, instance, validated_data):
        if 'translations' in validated_data:
            # Create/update/delete translations
            translations_data = validated_data.pop('translations', [])
            existing_translations = dict(map(lambda tr: (tr.id, tr), instance.translations.all()))
            translations_to_create = []
            translations_to_delete = []
            translations_to_update = []
            main_translation = None
            for trd in translations_data:
                is_main = trd.pop('is_main', False)
                if trd.get('id') in existing_translations:
                    tr = existing_translations[trd['id']]
                    if 'data' in trd:
                        tr.update_data(trd.pop('data'))
                    for k, v in trd.items():
                        setattr(tr, k, v)
                    translations_to_update.append(tr)
                else:
                    data = trd.pop('data', {})
                    tr = FindingTemplateTranslation(template=instance, **trd)
                    tr.pk = None
                    tr.update_data(data)
                    translations_to_create.append(tr)
                if is_main:
                    main_translation = tr
            for etr_id, etr in existing_translations.items():
                if not any(map(lambda tr: tr.get('id') == etr_id, translations_data)):
                    translations_to_delete.append(etr)

            # Update main_translation
            instance.main_translation = main_translation

            # Update cached risk_score. Inherit from main_translation if not overwritten
            for tr in translations_to_create + translations_to_update:
                tr.update_risk_score()

            # Update translations in DB
            if translations_to_delete:
                bulk_delete_with_history(FindingTemplateTranslation, translations_to_delete)
            if translations_to_update:
                bulk_update_with_history(
                    FindingTemplateTranslation,
                    translations_to_update,
                    fields=['language', 'status', 'custom_fields', 'title', 'risk_score', 'risk_level'],
                )
            if translations_to_create:
                bulk_create_with_history(FindingTemplateTranslation, translations_to_create)

        return super().update(instance, validated_data)


@extend_schema_field(OpenApiTypes.UUID)
class PentestProjectRelatedField(serializers.PrimaryKeyRelatedField):
    def get_queryset(self):
        return PentestProject.objects.only_permitted(self.context['request'].user)


class FindingTemplateFromPentestFindingSerializer(FindingTemplateSerializer):
    project = PentestProjectRelatedField(write_only=True)

    class Meta(FindingTemplateSerializer.Meta):
        fields = FindingTemplateSerializer.Meta.fields + ['project']

    @transaction.atomic
    @history_context()
    def create(self, validated_data):
        project = validated_data.pop('project')
        instance = super().create(validated_data | {'skip_post_create_signal': True})

        # Copy referenced images from project to template
        template_images = []
        for i in project.images.all():
            if any([f'/images/name/{i.name}' in str(tr.data) for tr in instance.translations.all()]):
                template_images.append(UploadedTemplateImage(
                    linked_object=instance,
                    name=i.name,
                    name_hash=i.name_hash,
                    file=i.file,
                ))
        bulk_create_with_history(UploadedTemplateImage, template_images)
        sysreptor_signals.post_create.send(sender=instance.__class__, instance=instance)
        return instance
